Repository navigation
Conversation
evcc-io/evcc#31995 adds a forced discharge battery mode for feed-in arbitrage and leaves the planning side open ("optimizer (MIP) integration to plan export windows from price spreads - deliberately out of scope"). This is the request side of that: a battery may now carry d_demand, the discharge energy per time step it is asked to deliver, mirroring the existing p_demand. The constraint is soft, like the charge demand it mirrors. A demand the battery cannot serve - already empty, d_max too small, the energy needed by the house - leaves an unserved remainder in the penalty variable instead of turning the request infeasible, and the penalty stays an order below prc_soc_exc_pen so a forced discharge stops at the s_min reserve rather than draining through it. The demand is clipped to d_max per step; reaching the grid still requires discharge_to_grid, the demand does not override it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ekkea
left a comment
There was a problem hiding this comment.
two things missing:
- missing check: p_demand and d_demand being >0 at the same time needs to be closed out. What shall happen? Reject and send 500?
- there is no precaution that d_demand may run into SOC becoming 0. Keeping up the penalty once this happens will lead to weird behavior as we have seen this with p_demand previously.
- i strongly recommend to add standard test cases (in form of requests that use this feature, added to the test_cases folder). They are much easier to review than specific test scripts.
# Conflicts: # src/optimizer/app.py
…ands A demand the battery cannot serve kept its penalty once the battery sat at s_min, the same pull at an empty battery the charge demand once had at a full one. z_s_min_reached releases the demand there. p_demand and d_demand in the same step are rejected with a 400 naming the battery and steps. Two data driven cases cover the served demand and the stop at the reserve. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@ekkea all three addressed in 91b6186, and the branch is merged up with main.
🤖 Generated with Claude Code |
|
Solve time, stored cases, three runs each, median, on this branch. Requests without Requests with
This is the price of the #95 construction, not of the mirror. The weakness is the big-M relaxation: What remains: the evcc side sends 🤖 Generated with Claude Code |
|
Two alternatives to the per step release binary, prototyped on this branch behind a switch, three runs each, median. Controls without any demand: 028 at 2.3 s, 020 at 1.5 s.
Monotone release keeps the binary and adds Export bonus drops the binary and the penalty: Two caveats. The adversarial every-4th 028 case goes to 70 s, all in the joint stage: the bonus interacts with that case's peak attenuation and The same trade exists for My take: the bonus fits 🤖 Generated with Claude Code |
Follow-up to the optimizer TODO in evcc-io/evcc#31995 ("optimizer (MIP) integration to plan export windows from price spreads — deliberately out of scope; this PR is the mode + site-logic foundation"). That PR added the
api.BatteryDischargemode andbatteryGridDischargeLimit; this one gives the optimizer a way to be told about it.API
New optional per-battery field
d_demand, the mirror of the existingp_demand:Added to the flask-restx model, the request parsing, the time-series length validation,
openapi.yaml, and the generated Go client (go generate ./...).Model
d_demand_pen[i][t], only for batteries that carry a demandd[i][t] + d_demand_pen[i][t] >= min(d_max * dt/3600, d_demand[t])-prc_p_goal_pen * d_demand_pen[i][t]in the objective, no time weighting: a forced discharge names the steps it wants itselfDeliberately soft, like the charge demand it mirrors. A demand the battery cannot serve — already empty,
d_maxtoo small, the energy needed by the house — leaves an unserved remainder rather than turning the request infeasible.prc_p_goal_pensits an order belowprc_soc_exc_pen, so the forced discharge stops at thes_minreserve instead of draining through it, which is the same reserve guardapplyBatteryModeenforces on the evcc side.Reaching the grid still requires
discharge_to_grid; the demand does not override it.Tests
tests/test_force_discharge.py: no demand leaves the battery alone, a demand is served against an unfavourable price spread, clipping tod_max, stopping ats_min, and staying solvable when there is no sink for the energy. Full suite green (103 passed), ruff clean.Review follow-up
p_demandandd_demandabove zero is rejected with a 400 naming the battery and the steps.z_s_min_reachedreleases the demand once the battery sits ats_min, the mirror ofz_s_max_reachedfor the charge demand, so the penalty no longer keeps pulling at an empty battery.029-forced-dischargeand030-forced-discharge-stops-at-reserveintest_cases/, both strict.🤖 Generated with Claude Code